package org.swift.mashup.engine.concurrency.memory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

import org.swift.commons.xml.Index;
import org.swift.mashup.engine.IndexedFile;

public class MappedMemoryManager {
	public static long PAGE_SIZE = 2048;
	private String id = "0";
	private File mapFile;
	private File memoryFile;
	private RandomAccessFile[] fileHandles;
	private FileChannel memoryFileChannel;
	private FileChannel mapFileChannel;
	private MappedByteBuffer memoryMapBuffer;
	private boolean readonly = false;
	public int position = 0;
	private boolean mappedFiles = true;
	private Object positionSemaphore = new Object();
	private static final String CONCURRENT_MAPPED_FILES = "org.swift.concurrent.MappedFiles";
	
	public MappedMemoryManager(){
		super();
		String shouldMap = System.getProperty(CONCURRENT_MAPPED_FILES);
		boolean mapped = (shouldMap==null)?true:Boolean.parseBoolean(shouldMap);
		this.mappedFiles=mapped;
	}

	public void open(String workingDirectory, String filename, boolean readonly) {
		File outputDir = new File(workingDirectory);
		outputDir.mkdirs();
		try {

			// Open input files
			this.mapFile = new File(outputDir, "mm-" + filename + ".map");
			this.memoryFile = new File(outputDir, "wm-" + filename + ".mem");
			// Open output files
			this.fileHandles = new RandomAccessFile[2];
			if (readonly) {
				if (this.memoryFile.exists()) {
					this.fileHandles[0] = new RandomAccessFile(this.memoryFile,
							"r");
					this.memoryFileChannel = this.fileHandles[0].getChannel();
					//toggle between mapped file and random files this will allow it to be a runtime configuration
					if (isMappedFiles()){
						this.memoryMapBuffer = this.memoryFileChannel.map(
								FileChannel.MapMode.READ_ONLY, 0, fileHandles[0]
										.length());
					}
					if (this.mapFile.exists()) {
						this.fileHandles[1] = new RandomAccessFile(
								this.mapFile, "r");
					}
				}
			} else {
				this.fileHandles[0] = new RandomAccessFile(this.memoryFile,
						"rw");
				if (this.fileHandles[0].length()==0) 
					this.fileHandles[0].setLength(PAGE_SIZE);
				this.memoryFileChannel = this.fileHandles[0].getChannel();
				//toggle between mapped file and random files this will allow it to be a runtime configuration
				if (isMappedFiles()){
					this.memoryMapBuffer = this.memoryFileChannel.map(
							FileChannel.MapMode.READ_WRITE, 0, this.fileHandles[0]
									.length());
				}
				this.fileHandles[1] = new RandomAccessFile(this.mapFile, "rw");
			}
			if (this.mapFile.exists())
				this.mapFileChannel = this.fileHandles[1].getChannel();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (RuntimeException e){
			e.printStackTrace();
		}
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public void close() {
		if (isMappedFiles()) {
			try {
				this.memoryMapBuffer.force();
			}
			catch(RuntimeException e) {
				e.printStackTrace();
			}
		}
		if (this.memoryFileChannel != null && this.memoryFileChannel.isOpen()){ 
			cleanupChannel(this.memoryFileChannel);
		}
		if (this.mapFileChannel != null && this.mapFileChannel.isOpen()) {
			cleanupChannel(this.mapFileChannel);
		}
		for (int i = 0; i < fileHandles.length; i++) {
			if (this.fileHandles[i]!=null)
				closeHandle(this.fileHandles[i]);
		}			
		this.memoryMapBuffer = null;
		this.fileHandles = null;
		this.memoryFileChannel = null;
		this.mapFileChannel = null;
	}

	private void cleanupChannel(FileChannel channel) {
		try {
			channel.force(false);
		}
		catch(IOException e) {
			e.printStackTrace();
		}
		finally {
			try {
				channel.close();
			}
			catch(IOException e) {
				e.printStackTrace();
			}
		}
	}

	private void closeHandle(RandomAccessFile handle) {
		try {			
			handle.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void delete(){
		try {
			this.mapFile.delete();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		try {
			this.memoryFile.delete();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}

	public FileChannel getMemoryFileChannel() {
		return memoryFileChannel;
	}

	public int checkAndReallocateBuffer(int bufferSize) {
		int currentPosition = this.memoryMapBuffer.position();
		if (this.readonly)
			return currentPosition;
		try {
			long currentSize = this.fileHandles[0].length();
			long requiredSize = currentPosition + bufferSize;
			long newSize = currentSize;
			if (requiredSize > currentSize) {
				while (newSize < requiredSize)
					newSize = newSize + PAGE_SIZE;
				this.fileHandles[0].setLength(newSize);
				this.memoryMapBuffer = this.memoryFileChannel.map(
						FileChannel.MapMode.READ_WRITE, 0, newSize);
				this.memoryMapBuffer.position(currentPosition);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return currentPosition;
	}

	public static void loadMemory(WorkingMemory memory, String memoryId,
			String fileIndex, int startIndex, int interval, boolean importMap) {
		BufferedReader memoryMapReader;
		String workingDir = memory.getWorkingDirectory();
		File outputDir = new File(workingDir);
		if (!outputDir.exists())
			return;
		try {
			// Open input files
			String filename = (fileIndex == "0") ? memoryId : memoryId + "-"
					+ fileIndex;
			File mapFile = new File(outputDir, "mm-" + filename + ".map");
			memoryMapReader = new BufferedReader(new FileReader(mapFile));
			// Load input files
			int index = 1;
			int overStartCount = 0;
			while (true) {
				String inputLine = memoryMapReader.readLine();
				if (inputLine == null)
					break;
				String[] itemParams = inputLine.split("\t");
				String memFileIndex = itemParams[4];
				if (!memory.getReadOnlyMemory().containsKey(memFileIndex)) {
					MappedMemoryManager memManager = new MappedMemoryManager();
					memManager.setId(memFileIndex);
					String memFilename = (memFileIndex == "0") ? memoryId
							: memoryId + "-" + memFileIndex;
					memManager.open(workingDir, memFilename, true);
					memory.getReadOnlyMemory().put(memFileIndex, memManager);
				}
				int id = Integer.parseInt(itemParams[0]);
				String type = itemParams[1];
				int position = Integer.parseInt(itemParams[2]);
				int length = Integer.parseInt(itemParams[3]);
				MemoryItem memItem = mapMemoryItem(memory, memFileIndex, id,
						type, position, length);
				if (memItem != null) {
					if (importMap) {
						/*
						 * importing a the map information is done when the main
						 * process pulls the memory items back from its
						 * sub-processes
						 */
						memory.insert(type, memItem);
						String mapRecord = memItem.getId() + "\t"
								+ memItem.type.getName() + "\t" + position
								+ "\t" + length + "\t" + memFileIndex + "\n";
						try {
							memory.getWriteableMemory().getMapFileChannel()
									.write(
											ByteBuffer.wrap(mapRecord
													.getBytes()));
						} catch (IOException e) {
							e.printStackTrace();
						}
					} else if (((index >= startIndex) && (overStartCount % interval) == 0)
							&& (interval != -1)) {
						memory.addItemToWorkingMemoryBuffer(memItem);
					} else if (index >= startIndex) {
						memory.addItemToLimbo(memItem);
					} else {
						memory.addItemToWorkingMemory(memItem);
					}
				}
				if (index >= startIndex)
					overStartCount++;
				index++;
			}
			memoryMapReader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 
	 * @param memory
	 * @param memoryId
	 * @param fileIndex
	 * @param type
	 * @param indexedFile
	 */
	public static void loadMemory(WorkingMemory memory, String memoryId,
			String fileIndex, String type, IndexedFile indexedFile) {
		String workingDir = memory.getWorkingDirectory();
		File outputDir = new File(workingDir);
		if (!outputDir.exists())
			return;
		if (!memory.getReadOnlyMemory().containsKey(fileIndex)) {
			MappedMemoryManager memManager = new MappedMemoryManager();
			memManager.setId(fileIndex);
			String memFilename = (fileIndex == "0") ? memoryId : memoryId + "-"
					+ fileIndex;
			// Rename file before opening if it is not named correctly
			File file = indexedFile.getFile();
			File newFile = new File(outputDir, "wm-" + memFilename + ".mem");
			if (!file.getAbsolutePath().equals(newFile.getAbsolutePath())) {
				file.renameTo(newFile);
			}
			memManager.open(workingDir, memFilename, true);
			memory.getReadOnlyMemory().put(fileIndex, memManager);
		}
		// Load input files
		while (indexedFile.hasNext()) {
			Index index = indexedFile.next();
			int id = -1;
			MemoryItem memItem = mapMemoryItem(memory, fileIndex, id, type,
					index.getPosition(), index.getLength());
			if (memItem != null) {
				memory.insert(type, memItem);
				String mapRecord = memItem.getId() + "\t"
						+ memItem.type.getName() + "\t" + index.getPosition()
						+ "\t" + index.getLength() + "\t" + fileIndex + "\n";
				try {
					memory.getWriteableMemory().getMapFileChannel().write(
							ByteBuffer.wrap(mapRecord.getBytes()));
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

		}
	}
	
	public Object getBytes(int position, int length) {
		synchronized (positionSemaphore){
    		if (this.isMappedFiles()){
    			MappedByteBuffer mappedBuffer = this.getMemoryMapBuffer();
    			if (mappedBuffer!=null){
    				int currentPosition = mappedBuffer.position();
    				//mappedBuffer.mark();
    				mappedBuffer.position(position);
    				byte[] dst = new byte[length];
    				mappedBuffer.get(dst);
    				try {
    					mappedBuffer.position(currentPosition);
    					//mappedBuffer.reset();
    				} catch (Exception e) {
    					// If mark is bad allow the position to stay as it is
    					System.out.println(e.getMessage());
    				}
    				return dst;
    			}
    		} else {
    			byte[] dst = new byte[length];
    			ByteBuffer buf = ByteBuffer.wrap(dst);
    			try {
    				this.getMemoryFileChannel().read(buf, position);
    				return dst;
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    		return new byte[]{};
		}
	}
	
	public void putBytes(Object value) {
		synchronized (positionSemaphore){
			if (this.isMappedFiles()){
				MappedByteBuffer mappedBuffer = this.getMemoryMapBuffer();
				if(value instanceof ByteBuffer){
					ByteBuffer buf = (ByteBuffer)value;
					mappedBuffer.put(buf);
				} else if (value instanceof byte[]){
					byte[] buf = (byte[])value;
					mappedBuffer.put(ByteBuffer.wrap(buf));
				} else if (value instanceof String){
					String buf = (String)value;
					mappedBuffer.put(ByteBuffer.wrap(buf.getBytes()));
				} else if (value instanceof Integer){
					mappedBuffer.putInt(((Integer)value).intValue());
				} else if (value instanceof Long){
					mappedBuffer.putLong(((Long)value).longValue());
				} else if (value instanceof Boolean){
					mappedBuffer.put((byte)(((Boolean)value)?1:0));
				} else if (value instanceof Double){
					mappedBuffer.putDouble(((Double)value).doubleValue());
				}
			} else {
				ByteBuffer buf = getByteBuffer(value);
				try {
					this.getMemoryFileChannel().write(buf);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	private ByteBuffer getByteBuffer(Object value) {
		if(value instanceof ByteBuffer){
			ByteBuffer buf = (ByteBuffer)value;
			return buf;
		} else if (value instanceof byte[]){
			byte[] buf = (byte[])value;
			return ByteBuffer.wrap(buf);
		} else if (value instanceof String){
			String buf = (String)value;
			return ByteBuffer.wrap(buf.getBytes());
		} else if (value instanceof Integer){
			return ByteBuffer.wrap(((Integer)value).toString().getBytes());
		} else if (value instanceof Long){
			return ByteBuffer.wrap(((Long)value).toString().getBytes());
		} else if (value instanceof Boolean){
			return ByteBuffer.wrap(((String)((((Boolean)value).booleanValue())?"1":"0")).getBytes());
		} else if (value instanceof Double){
			return ByteBuffer.wrap(((Double)value).toString().getBytes());
		}
		return null;
	}
	
	public int getObjectLength(Object value){
		int length = 0;
		if(value instanceof ByteBuffer){
			ByteBuffer buf = (ByteBuffer)value;
			length = buf.limit();
		} else if (value instanceof byte[]){
			byte[] buf = (byte[])value;
			length = buf.length;
		} else if (value instanceof String){
			String buf = (String)value;
			length = buf.length();
		} else if (value instanceof Integer){
			length = ((Integer)value).toString().length();
		} else if (value instanceof Long){
			length = ((Long)value).toString().length();
		} else if (value instanceof Boolean){
			length = 1;
		} else if (value instanceof Double){
			length = ((Double)value).toString().length();
		}
		return length;
	}

	private static MemoryItem mapMemoryItem(WorkingMemory memory,
			String fileIndex, int id, String type, int startPosition, int length) {
		MappedMemoryManager readmanager = memory.getReadOnlyMemory().get(
				fileIndex);		
		if (readmanager == null)
			return null;
		MemoryType memType = memory.getMemoryTypes().get(type);
		MemoryItem memItem = null;
		if (memType.getName().startsWith("collect(")) {
			memItem = new CollectionMemoryItem(id, memType, memory, true);
		} else {
			memItem = new PersistentMemoryItem(id, memType, startPosition,
					length, readmanager, memory);
		}
		return memItem;
	}

	public FileChannel getMapFileChannel() {
		return mapFileChannel;
	}

	public void setMapFileChannel(FileChannel mapFileChannel) {
		this.mapFileChannel = mapFileChannel;
	}

	public MappedByteBuffer getMemoryMapBuffer() {
		return memoryMapBuffer;
	}

	public boolean isMappedFiles() {
		return mappedFiles;
	}

	public void setMappedFiles(boolean mappedFiles) {
		this.mappedFiles = mappedFiles;
	}
}
